/*
 * Copyright 2007 - The JDPF Project Team (http://www.jdpf.org) 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package org.jdpf.core.plugins.parameters;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.StringTokenizer;

import org.jdpf.core.kernel.exceptions.CoreException;
import org.jdpf.core.kernel.exceptions.CoreExceptionsEnum;
import org.jdpf.core.kernel.plugins.BundleToolBox;

/**
 * This class evaluates the correctness of the plugins manifest.
 * @author Paolo Ciccarese
 * @author Cristiana Larizza
 *
 */
public class ManifestParameterValidator extends AbstractParametersValidator {
	
	public ManifestParameterValidator(boolean logging, String pluginId) {
		super(logging, pluginId);
	}
	
	/**
	 * Given the parameter name, this method is able to look up in the manifest 
	 * and read the declared type. The pluginId for detecting the right plugin
	 * has to be declared in the constructor.
	 * @param paramName	The name of the parameter 
	 * @return	The string representation of the type.
	 * @throws CoreException
	 */
	public String getParameterTypeAsInTheManifest(String paramName) throws CoreException {
		// Parameters in the manifest have the prefix 'Param-'
		String manifestValue = (String) BundleToolBox.getDefault().getHeaders(pluginId)
			.get("Param-"+paramName);
		
		if(manifestValue!=null) {
			String[] tokens = manifestValue.split(";");
			if(tokens.length>0) {
				StringTokenizer st = new StringTokenizer (tokens[0]);
				String paramType = st.nextToken();
				return paramType;
			}
			throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
					"The parameter value '" + paramName + "' is not valid.");
		} else
			throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
					"The parameter value '" + paramName + "' is not declared in the manifest and will be ignored.");
	}
	
	/**
	 * Checks if the parameters in the manifest of the specific plugin are
	 * syntactically correct.
	 * @throws CoreException
	 * @throws IOException 
	 */
	public void checkParameter() throws CoreException{
		if (logging) logger.logMainPoint(this, pluginId, "Parameters manifest validation started.");
		
		Dictionary dictionary = BundleToolBox.getDefault()
			.getHeaders(pluginId);
		
		Enumeration manifestKey = dictionary.keys();
		while (manifestKey.hasMoreElements()) {
			String key = (String) manifestKey.nextElement();
			if (key.startsWith("Param-")) {
				String paramName = key.substring(6);
				String paramValue = (String) dictionary.get(key);
				
				if (logging) logger.log(this, pluginId, "Check manifest parameter: " + paramName + " with manifest value " + paramValue);
				
				String[] tokens = paramValue.split(";");
				// tokens[0] contains the parameter specification as 'string optional'
				if(tokens.length>0) {
					String[] paramSpec = tokens[0].split(" ");
					String paramType = paramSpec[0];
					
					// Checks if the type of the parameter is allowed
					ParametersTypesEnum parameterType = parameterTypeValidation(paramType);
					// Checks if the mandatory flag is correct
					if(paramSpec.length>1) isParameterMandatory(paramSpec[1].trim());
					// Checks the constraints
					if(tokens.length>2) 
						logger.log(this, pluginId, "Check manifest parameter: " + paramName + " with manifest value " + paramValue + ". It has too many constraints." +
								" Only the first set of constraints will be considered.");
					if(tokens.length>1) {
						// tokens[1] contains the parameter range as '[a,b]' or
						// the parameter allowed values as '{3,5,6,8}
						if(tokens[1].trim().startsWith("{") && tokens[1].trim().endsWith("}")){
							allowedValuesAgainstTheTypeValidation(parameterType, tokens[1].trim().substring(1, tokens[1].trim().length()-1));
						} else if ((tokens[1].trim().startsWith("[") || tokens[1].trim().startsWith("]") )
								&& (tokens[1].trim().endsWith("]") || tokens[1].trim().endsWith("["))){
							parameterAllowedRangeSyntaxValidation(parameterType, tokens[1].trim().substring(1, tokens[1].trim().length()-1));
						} else 	
							logger.log(this, pluginId, "Check manifest parameter: " + paramName + " with manifest value " + paramValue + ". It has not valid constraints.");
					} 
//					else if(tokens.length>2) {
//						logger.log(this, pluginId, "Check manifest parameter: " + paramName + " with manifest value " + paramValue + ". It has too many constraints." +
//								" Only the first set of constraints will be considered.");
//						if(tokens[1].trim().startsWith("{") && tokens[1].trim().endsWith("}")){
//							allowedValuesAgainstTheTypeValidation(parameterType, tokens[1].trim().substring(1, tokens[1].trim().length()-1));
//						} else if (tokens[1].trim().startsWith("[") && tokens[1].trim().endsWith("]")){
//							parameterAllowedRangeSyntaxValidation(parameterType, tokens[1].trim().substring(1, tokens[1].trim().length()-1));
//						}	
//					}
				} else {
					throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
							"The parameter value has to include at least the parameter type: " +
							"{string,int,float,double, boolean}");
				}
			}
		}
		
		if (logging) logger.logMainPoint(this, pluginId, "Parameters manifest validation completed.");
	}
	
	/**
	 * It verifies if the specified ranges are allowed and written with the right syntax. 
	 * @param parameterType	The type of the parameter under evaluation
	 * @param values	The string value of the range.
	 * @throws CoreException
	 */
	private void parameterAllowedRangeSyntaxValidation(ParametersTypesEnum parameterType, String values) throws CoreException {
		if(!ParametersTypesEnum.isParameterTypeNumeric(parameterType)) 
			throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
					"The parameter allowed range is allowed only for numerivc types, not for '" +
					parameterType.getStringValue() + "'");
		
		String[] tokens = values.split(",");
		if(tokens.length == 0 || tokens.length > 2)
			throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
				"The parameter allowed range [" + values + "] is not valid. A range has to be " +
				"defined in the format [a, b] with a<b or [a,] or [,b]");
		try {
			int count = 0;
			for (String token: tokens) {
				if(token.trim().length()>0) {
					Double.parseDouble(token);
					count++;
				}
			}
			if(count==2) {
				if (Double.parseDouble(tokens[0])>=Double.parseDouble(tokens[1]))
					throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
							"A range has to be defined in the format [a, b] with a<b");
			}
		} catch (NumberFormatException e) {
			throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
				"The parameter allowed range [" + values + "] should only include numbers. A range has to be " +
				"defined in the format [a, b] with a<b or [a,] or [,b]");
		}
	}	
	
	/**
	 * This is checking that the list of allowed values is compatible with the
	 * declared data type. 
	 * @param parameterType	The actual parameter data type
	 * @param values	The string of all the allowed values
	 * @throws CoreException
	 * @throws IOException 
	 */
	private void allowedValuesAgainstTheTypeValidation(ParametersTypesEnum parameterType, String values) throws CoreException {
		StringTokenizer st1 = new StringTokenizer(values, ",");
		if(ParametersTypesEnum.isParameterTypeNumeric(parameterType)) {
			while (st1.hasMoreTokens()) {
				String paramAllowedValue = st1.nextToken();
				try {
					if (parameterType.equals(ParametersTypesEnum.Int)) {
						Integer.parseInt((String) paramAllowedValue);
					} else if (parameterType.equals(ParametersTypesEnum.Double)) {
						Double.parseDouble((String) paramAllowedValue);
					} else if (parameterType.equals(ParametersTypesEnum.Float)) {
						Float.parseFloat((String) paramAllowedValue);
					} 
				} catch (NumberFormatException e) {
					throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
						"The parameter allowed value " + paramAllowedValue + " is not valid. Allowed " +
						"values has to be: " + parameterType);
				}
			}
		} else if (parameterType.equals(ParametersTypesEnum.Boolean)) {
			logger.log(this, pluginId, "The allowed boolean values are {true, false}. Any value restriction will be ignored!");
		} else if (parameterType.equals(ParametersTypesEnum.String)) {
			// TODO nothing
		} else if (parameterType.equals(ParametersTypesEnum.URI)) {
			while(st1.hasMoreTokens()){
				String paramAllowedValue = st1.nextToken();
				try {
					new URI(paramAllowedValue);
				} catch (URISyntaxException e) {
					throw new CoreException (CoreExceptionsEnum.IllegalManifestParameters,
							"The parameter allowed value " + paramAllowedValue + " is not valid. Allowed " +
							"values has to be: " + parameterType);
				} 
			}
		}
	}
}
